/**
 * This file contains logic to grayscale a PNG from a ReadableStream.
 */


/**
 * Remove Paeth filter from three data points.
 */
function paeth (a, b, c) {
  const p = a + b - c;
  const pa = Math.abs(p - a);
  const pb = Math.abs(p - b);
  const pc = Math.abs(p - c);
  if (pa <= pb && pa <= pc) return a;
  if (pb <= pc) return b;

  return c;
}


/**
 * A transformer of a TransformStream to create a grayscale PNG.
 */
class GrayscalePNGTransformer {
  constructor() {
    this._mode = 'magic';


    this._buffer = new Uint8Array(0);
    this._IHDRLength = 25; // IHDR块固定长度
  }

  /**
   * Called for every downloaded PNG data chunk to be grayscaled.
   *
   * @param {Uint8Array} chunk The downloaded chunk.
   * @param {TransformStreamDefaultController} controller The controller to euqueue grayscaled data.
   * 之前存在的问题
   * - 你只基于当前 chunk 创建 source，但 PNG 块（尤其是 IDAT）通常跨多个流片段，不可能在一次 transform() 就完整读完；
   * - 然而你创建的 source 和 target 是当前 chunk 范围，而不是 data（缓存 + 当前）的范围；
   * - 同时你后面修改的是 buffer，但 controller.enqueue(buffer.subarray(...)) 是最后才调用的，中间处理过程没有连续拼接所有数据，且写入逻辑也不合理。
   * - 魔数magic 和 header 的解析是基于 chunk 的 offset，不是整个 data 的偏移
   * - 你现在的写法会假设所有 IDAT 数据在一个 chunk 中读完并压缩并写回，然后就结束处理，这是不符合真实流处理方式的。尤其是 PNG 的 IDAT 可以多个，且数据量较大。
   *
   */
  transform (chunk, controller) {
    // 拼接上次剩余数据
    let data = new Uint8Array(this._buffer.length + chunk.length);

    data.set(this._buffer, 0);
    data.set(chunk, this._buffer.length);

    // let position = chunk.byteOffset;
    // let length = chunk.byteLength;
    // const source = new DataView(chunk.buffer, position, length);
    // const buffer = new Uint8Array(length);
    // const target = new DataView(buffer.buffer, position, length);



    let position = 0;

    while (position < data.length) {
      switch (this._mode) {
        case 'magic': {
          if (data.length < 8) break; // 等待更多数据

          // 解析 magic
          this._mode = 'header';
          position += 8;
          break;

          // const magic1 = source.getUint32(position);
          // target.setUint32(position, magic1);
          // position += 4;

          // const magic2 = source.getUint32(position);
          // target.setUint32(position, magic2);
          // position += 4;

          // const magic = magic1.toString(16) + '0' + magic2.toString(16);
          // console.log('%cPNG magic:     %c %o', 'font-weight: bold', '', magic);
          // if (magic !== '89504e470d0a1a0a') {
          //   throw new TypeError('不是 png 文件');
          // }

          // this._mode = 'header';
          // break;
        }
        case 'header': {
          if (data.length - position < 25) break; // 等待 IHDR 块完整

          const view = new DataView(data.buffer, data.byteOffset + position);
          const chunkLength = view.getUint32(0); // 13
          const chunkType = readString(data, position + 4, 4);

          if (chunkType !== 'IHDR') throw new Error('Invalid PNG, missing IHDR');

          this._width = view.getUint32(8);
          this._height = view.getUint32(12);
          this._bitDepth = view.getUint8(16);
          this._colorType = view.getUint8(17);

          // Save IHDR chunk (including CRC) to replay later
          this._chunks = [];
          this._chunks.push(data.subarray(position, position + 25));

          position += 25;
          this._mode = 'data';
          break;
        }
        case 'data': {
          // 这里只能处理一个完整 IDAT 块
          if (data.length - position < 8) break; // 等待长度 + 名称
          const chunkLength = readUint32(data, position);
          const chunkType = readString(data, position + 4, 4);

          // 如果不是 IDAT，就切换到 'end' 状态
          if (chunkType !== 'IDAT') {
            this._mode = 'end';
            break;
          }

          // 检查是否有足够数据：长度（4）+ 类型（4）+ 数据 + CRC（4）
          if (data.length - position < 8 + chunkLength + 4) break; // 不完整，等待更多数据、

          // 获取 IDAT 块的数据部分
          const idatData = data.subarray(position + 8, position + 8 + chunkLength);

          // 解压 IDAT 块（PNG 使用 zlib）
          const decompressed = pako.inflate(idatData);

          const bytesPerCol = this.bytesPerPixel();
          const bytesPerRow = this._width * bytesPerCol + 1;

          const unfiltered = this.removeFilters(decompressed, bytesPerCol, bytesPerRow)
          const grayscaled = this.grayscale(unfiltered, bytesPerCol, bytesPerRow);
          const recompressed = pako.deflate(grayscaled);

          // 拼装 IDAT 块，写入 target 缓冲区
          const output = buildChunk('IDAT', recompressed);
          controller.enqueue(output);

          position += 8 + chunkLength + 4;
          this._mode = 'end';
          break;


          // // Read chunk info
          // const dataSize = source.getUint32(position);
          // console.log('%cPNG data size: %c %d', 'font-weight: bold', '', dataSize);

          // const chunkName = this.readString(source, position + 4, 4);
          // if (chunkName !== 'IDAT') {
          //   throw new TypeError('PNG is missing IDAT chunk');
          // }

          // const crcStart = position + 4;

          // // Extract the data from the PNG stream
          // const bytesPerCol = this.bytesPerPixel();
          // const bytesPerRow = this._width * bytesPerCol + 1;
          // let result = chunk.subarray(position + 8, position + 8 + dataSize);

          // // Decompress the data
          // result = pako.inflate(result);

          // // Remove PNG filters from each scanline
          // result = this.removeFilters(result, bytesPerCol, bytesPerRow);

          // // Actually grayscale the image
          // result = this.grayscale(result, bytesPerCol, bytesPerRow);

          // // Compress with Deflate
          // result = pako.deflate(result);

          // // Write data to target
          // target.setUint32(position, result.byteLength);
          // this.writeString(target, position + 4, 'IDAT');
          // buffer.set(result, position + 8);

          // position += result.byteLength + 8;

          // const chunkCrc = crc32(chunkName, result);
          // target.setUint32(position, chunkCrc);
          // position += 4;

          // this._mode = 'end';
          // break;
        }
        case 'end': {
          console.log('结束了')
          // 等待 IEND 块，不做改动直接透传
          if (data.length - position < 12) break;

          controller.enqueue(data.subarray(position, position + 12));
          position += 12;
          break;


          // // Write IEND chunk
          // target.setUint32(position, 0);
          // position += 4;
          // this.writeString(target, position, 'IEND');
          // position += 4;
          // target.setUint32(position, 2923585666);
          // position += 4;

          // controller.enqueue(buffer.subarray(0, position));
          // return;
        }
      }
    }


    // 缓存未处理的部分
    this._buffer = data.subarray(position);
  }

  /**
   * @param {DataView} dataView
   * @param {number} position
   * @param {number} length
   */
  readString (dataView, position, length) {
    return new Array(length)
      .fill(0)
      .map((e, index) => String.fromCharCode(dataView.getUint8(position + index))).join('');
  }

  /**
   * @param {DataView} dataView
   * @param {number} position
   * @param {string} string
   */
  writeString (dataView, position, string) {
    string.split('').forEach((char, index) => dataView.setUint8(position + index, char.charCodeAt(0)));
  }

  /**
   * Retrieves the PNG's color type.
   *
   * @param {number} number A number representing the color type.
   * @return {string} A string naming the color type.
   */
  colorType (number) {
    switch (number) {
      case 0: return 'grayscale';
      case 2: return 'rgb';
      case 3: return 'palette';
      case 4: return 'grayscale-alpha';
      case 6: return 'rgb-alpha';
    }
  }

  /**
   * Returns the number of bytes to read per pixel.
   *
   * @return {number}
   */
  bytesPerPixel () {
    if (this._bitDepth < 8) {
      throw new Error('Bit depths below 8 bit are currently not supported.');
    }

    const byteDepth = this._bitDepth / 8;
    if (this._colorType === 0) {
      return byteDepth;
    }
    if (this._colorType === 2) {
      return 3 * byteDepth;
    }
    if (this._colorType === 3) {
      return byteDepth;
    }
    if (this._colorType === 4) {
      return 2 * byteDepth;
    }

    return 4 * byteDepth;
  }

  removeFilters (src, bytesPerCol, bytesPerRow) {
    const dest = src.slice();
    for (let y = 0, i = y * bytesPerRow; y < this._height; y += 1, i = y * bytesPerRow) {
      const filter = src[i];
      dest[i] = 0;
      i++;
      if (filter === 1) {
        // Sub filter
        for (let x = 0, j = i; x < this._width; x += 1, j += bytesPerCol) {
          dest[j] = src[j] + dest[j - bytesPerCol];
          dest[j + 1] = src[j + 1] + dest[j + 1 - bytesPerCol];
          dest[j + 2] = src[j + 2] + dest[j + 2 - bytesPerCol];
          dest[j + 3] = src[j + 3] + dest[j + 3 - bytesPerCol];
        }
      } else if (filter === 2) {
        // Up filter
        for (let x = 0, j = i; x < this._width; x += 1, j += bytesPerCol) {
          dest[j] = src[j] + dest[j - bytesPerRow];
          dest[j + 1] = src[j + 1] + dest[j + 1 - bytesPerRow];
          dest[j + 2] = src[j + 2] + dest[j + 2 - bytesPerRow];
          dest[j + 3] = src[j + 3] + dest[j + 3 - bytesPerRow];
        }
      } else if (filter === 3) {
        // Average filter
        // for (let x = 0, j = i; x < this._width; x += 1, j += bytesPerCol) {
        //   dest[j] = src[j] + Math.trunc((dest[j - bytesPerCol] + dest[j - bytesPerRow]) / 2);
        //   dest[j + 1] = src[j + 1] + Math.trunc((dest[j + 1 - bytesPerCol] + dest[j + 1 - bytesPerRow]) / 2);
        //   dest[j + 2] = src[j + 2] + Math.trunc((dest[j + 2 - bytesPerCol] + dest[j + 2 - bytesPerRow]) / 2);
        //   dest[j + 3] = src[j + 3] + Math.trunc((dest[j + 3 - bytesPerCol] + dest[j + 3 - bytesPerRow]) / 2);
        // }

        for (let x = 0, j = i; x < this._width; x += 1, j += bytesPerCol) {
          for (let k = 0; k < bytesPerCol; k++) {
            const left = x === 0 ? 0 : dest[j + k - bytesPerCol];
            const up = y === 0 ? 0 : dest[j + k - bytesPerRow];
            dest[j + k] = (src[j + k] + Math.floor((left + up) / 2)) & 0xFF;
          }
        }
      } else if (filter === 4) {
        // Paeth filter
        // for (let x = 0, j = i; x < this._width; x += 1, j += bytesPerCol) {
        //   dest[j] = src[j] + paeth(dest[j - bytesPerCol], dest[j - bytesPerRow], dest[j - bytesPerCol - bytesPerRow]);
        //   dest[j + 1] = src[j + 1] + paeth(dest[j + 1 - bytesPerCol], dest[j + 1 - bytesPerRow], dest[j + 1 - bytesPerCol - bytesPerRow]);
        //   dest[j + 2] = src[j + 2] + paeth(dest[j + 2 - bytesPerCol], dest[j + 2 - bytesPerRow], dest[j + 2 - bytesPerCol - bytesPerRow]);
        //   dest[j + 3] = src[j + 3] + paeth(dest[j + 3 - bytesPerCol], dest[j + 3 - bytesPerRow], dest[j + 3 - bytesPerCol - bytesPerRow]);
        // }

        for (let x = 0, j = i; x < this._width; x += 1, j += bytesPerCol) {
          for (let k = 0; k < bytesPerCol; k++) {
            const a = x === 0 ? 0 : dest[j + k - bytesPerCol]; // left
            const b = y === 0 ? 0 : dest[j + k - bytesPerRow]; // up
            const c = (x === 0 || y === 0) ? 0 : dest[j + k - bytesPerRow - bytesPerCol]; // up-left
            dest[j + k] = (src[j + k] + paeth(a, b, c)) & 0xFF;
          }
        }
      }
    }
    return dest;
  }

  // grayscale (src, bytesPerCol, bytesPerRow) {
  //   const dest = src.slice();
  //   for (let y = 0, i = y * bytesPerRow; y < this._height; y += 1, i = y * bytesPerRow) {
  //     const filter = src[i];
  //     if (filter === 0) {
  //       i++;
  //       for (let x = 0, j = i; x < this._width; x += 1, j += bytesPerCol) {
  //         const red = src[j];
  //         const green = src[j + 1];
  //         const blue = src[j + 2];

  //         const gray = Math.trunc((red + green + blue) / 3);
  //         dest[j] = gray;
  //         dest[j + 1] = gray;
  //         dest[j + 2] = gray;
  //       }
  //     }
  //   }
  //   return dest;
  // }

  grayscale (src, bytesPerCol, bytesPerRow) {
    const result = src.slice();
    for (let y = 0, i = y * bytesPerRow; y < this._height; y += 1, i = y * bytesPerRow) {
      i++; // skip filter byte
      for (let x = 0; x < this._width; x++) {
        const idx = i + x * bytesPerCol;
        const r = result[idx];
        const g = result[idx + 1];
        const b = result[idx + 2];
        const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
        result[idx] = result[idx + 1] = result[idx + 2] = gray;
      }
    }
    return result;
  }
}